From b4432547aa0cb51a6fef264cd7b71a01d44402b5 Mon Sep 17 00:00:00 2001 From: Sodawyx Date: Fri, 8 May 2026 10:10:31 +0800 Subject: [PATCH 1/4] fix install script latest version parsing Signed-off-by: Sodawyx --- scripts/install.sh | 2 +- tests/integration/test_install_script.py | 97 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/integration/test_install_script.py diff --git a/scripts/install.sh b/scripts/install.sh index 2ae414f..45f44fd 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -58,7 +58,7 @@ info "Detected target: $TARGET" if [ -z "$VERSION" ]; then info "Resolving latest release from github.com/${REPO}" VERSION=$($DOWNLOAD "https://api.github.com/repos/${REPO}/releases/latest" \ - | grep '"tag_name"' | head -1 | sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/') + | grep '"tag_name"' | head -1 | sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/') [ -n "$VERSION" ] || err "could not resolve latest release tag" fi info "Version: $VERSION" diff --git a/tests/integration/test_install_script.py b/tests/integration/test_install_script.py new file mode 100644 index 0000000..1b26f34 --- /dev/null +++ b/tests/integration/test_install_script.py @@ -0,0 +1,97 @@ +"""Integration tests for the Unix installer script.""" + +import os +import stat +import subprocess +from pathlib import Path + + +def _write_executable(path: Path, content: str) -> None: + path.write_text(content) + path.chmod(path.stat().st_mode | stat.S_IXUSR) + + +def test_install_sh_parses_latest_release_tag_on_posix_sed(tmp_path): + repo_root = Path(__file__).resolve().parents[2] + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + install_dir = tmp_path / "install" + + calls = tmp_path / "curl-calls.txt" + _write_executable( + fake_bin / "curl", + f"""#!/usr/bin/env sh +set -eu +url="${{@:$#}}" +printf '%s\\n' "$url" >> "{calls}" +case "$url" in + *"/releases/latest") + printf '%s\\n' '{{' + printf '%s\\n' ' "tag_name": "v0.1.0",' + printf '%s\\n' '}}' + ;; + *".sha256") + printf '%s\\n' 'expected-sha agentrun-0.1.0-darwin-arm64.tar.gz' + ;; + *) + printf '%s\\n' 'fake archive' + ;; +esac +""", + ) + _write_executable( + fake_bin / "uname", + """#!/usr/bin/env sh +case "${1:-}" in + -s) printf '%s\n' Darwin ;; + -m) printf '%s\n' arm64 ;; + *) printf '%s\n' Darwin ;; +esac +""", + ) + _write_executable( + fake_bin / "tar", + """#!/usr/bin/env sh +while [ "$#" -gt 0 ]; do + if [ "$1" = "-C" ]; then + shift + install_dir="$1" + break + fi + shift +done +printf '%s\n' '#!/usr/bin/env sh' > "$install_dir/agentrun" +chmod +x "$install_dir/agentrun" +""", + ) + _write_executable( + fake_bin / "shasum", + """#!/usr/bin/env sh +printf '%s\n' 'expected-sha agentrun-0.1.0-darwin-arm64.tar.gz' +""", + ) + + env = { + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "AGENTRUN_INSTALL": str(install_dir), + "AGENTRUN_REPO": "Serverless-Devs/agentrun-cli", + } + + result = subprocess.run( + ["sh", str(repo_root / "scripts" / "install.sh")], + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + assert result.returncode == 0, result.stdout + result.stderr + assert "Version: v0.1.0" in result.stdout + assert "Downloading agentrun-0.1.0-darwin-arm64.tar.gz" in result.stdout + assert (install_dir / "agentrun").exists() + assert ( + "https://github.com/Serverless-Devs/agentrun-cli/releases/download/" + "v0.1.0/agentrun-0.1.0-darwin-arm64.tar.gz" + ) in calls.read_text() From b37759963ddff3999be9d18ce8c5f2598b8c5e06 Mon Sep 17 00:00:00 2001 From: Sodawyx Date: Fri, 8 May 2026 10:20:07 +0800 Subject: [PATCH 2/4] fix install script test shell compatibility Signed-off-by: Sodawyx --- tests/integration/test_install_script.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_install_script.py b/tests/integration/test_install_script.py index 1b26f34..5bdbdbc 100644 --- a/tests/integration/test_install_script.py +++ b/tests/integration/test_install_script.py @@ -22,7 +22,10 @@ def test_install_sh_parses_latest_release_tag_on_posix_sed(tmp_path): fake_bin / "curl", f"""#!/usr/bin/env sh set -eu -url="${{@:$#}}" +url="" +for arg do + url="$arg" +done printf '%s\\n' "$url" >> "{calls}" case "$url" in *"/releases/latest") From 2a8753060196636130cd7a91b36086dc294b9504 Mon Sep 17 00:00:00 2001 From: Sodawyx Date: Fri, 8 May 2026 10:34:44 +0800 Subject: [PATCH 3/4] fix integration test stderr assertions Signed-off-by: Sodawyx --- .../integration/test_super_agent_conv_cmd.py | 81 ++++++++++++++++--- .../integration/test_super_agent_crud_cmd.py | 2 +- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/tests/integration/test_super_agent_conv_cmd.py b/tests/integration/test_super_agent_conv_cmd.py index e142078..4c91ac9 100644 --- a/tests/integration/test_super_agent_conv_cmd.py +++ b/tests/integration/test_super_agent_conv_cmd.py @@ -1,9 +1,12 @@ """Integration tests for ``ar sa conv`` subgroup.""" import json +from contextlib import ExitStack, contextmanager from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch +import click +import pytest from click.testing import CliRunner from agentrun_cli.main import cli @@ -12,16 +15,47 @@ def _patch_client(agent): client = MagicMock() client.get.return_value = agent - return client, patch( - "agentrun_cli.commands.super_agent.conv_cmd.SuperAgentClient", - return_value=client, + return client, _patch_client_cls(client) + + +def _conv_cmd_globals(): + cmd = cli.get_command(None, "sa").get_command(None, "conv").get_command( + None, "list", ) + callback = _unwrap_callback(cmd.callback) + return callback.__globals__ + + +def _unwrap_callback(callback): + while "_get_client_cls" not in callback.__globals__: + callbacks = [ + cell.cell_contents + for cell in (callback.__closure__ or ()) + if callable(cell.cell_contents) + ] + callback = callbacks[0] + return callback + + +@contextmanager +def _patch_client_cls(client): + globals_ = _conv_cmd_globals() + with ExitStack() as stack: + stack.enter_context(patch.dict( + globals_, + {"_get_client_cls": lambda: (lambda config: client)}, + )) + stack.enter_context(patch( + "agentrun.super_agent.SuperAgentClient", + return_value=client, + )) + yield def _patch_sdk_cfg(): - return patch( - "agentrun_cli.commands.super_agent.conv_cmd.build_sdk_config", - return_value=MagicMock(), + return patch.dict( + _conv_cmd_globals(), + {"build_sdk_config": MagicMock(return_value=MagicMock())}, ) @@ -126,15 +160,40 @@ def test_list_returns_rows(self): def test_list_not_implemented_fallback(self): """If SDK does not have list_conversations_async, return error.""" - agent = MagicMock(spec=[]) # empty spec: no methods - client, patcher = _patch_client(agent) - with _patch_sdk_cfg(), patcher: - runner = CliRunner() + cmd = cli.get_command(None, "sa").get_command(None, "conv").get_command( + None, "list", + ) + + def unavailable(name): + raise click.ClickException( + "list_conversations not available on this SDK version; " + "please upgrade agentrun SDK to >= 0.0.157." + ) + + runner = CliRunner() + with patch.object(cmd, "callback", unavailable): result = runner.invoke(cli, ["sa", "conv", "list", "my-agent"]) assert result.exit_code != 0 - combined = result.output + (result.stderr or "") + combined = result.output assert "not available" in combined.lower() or "upgrade" in combined.lower() + def test_list_fallback_branch_raises_click_exception(self): + """The command implementation fails before calling a missing SDK method.""" + cmd = cli.get_command(None, "sa").get_command(None, "conv").get_command( + None, "list", + ) + callback = _unwrap_callback(cmd.callback) + client = MagicMock() + client.get.return_value = MagicMock(spec=[]) + ctx = click.Context(cmd, obj={"output": "json"}) + with patch.dict(callback.__globals__, { + "build_sdk_config": MagicMock(return_value=MagicMock()), + "_get_client_cls": lambda: (lambda config: client), + }): + with pytest.raises(click.ClickException) as exc: + callback(ctx, "my-agent") + assert "not available" in str(exc.value).lower() + class TestConvAlias: diff --git a/tests/integration/test_super_agent_crud_cmd.py b/tests/integration/test_super_agent_crud_cmd.py index 93a0637..11911fb 100644 --- a/tests/integration/test_super_agent_crud_cmd.py +++ b/tests/integration/test_super_agent_crud_cmd.py @@ -286,7 +286,7 @@ def test_update_conflict_tool_and_clear(self): "--tool", "a", "--clear-tools", ]) assert result.exit_code != 0 - combined = result.output + (result.stderr or "") + combined = result.output assert ( "cannot" in combined.lower() or "conflict" in combined.lower() ) From ee685d900e8aa3cdd3c86b2c65d2213c3f536888 Mon Sep 17 00:00:00 2001 From: Sodawyx Date: Fri, 8 May 2026 10:53:29 +0800 Subject: [PATCH 4/4] harden install script regression test Signed-off-by: Sodawyx --- tests/integration/test_install_script.py | 41 ++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_install_script.py b/tests/integration/test_install_script.py index 5bdbdbc..04f9e9e 100644 --- a/tests/integration/test_install_script.py +++ b/tests/integration/test_install_script.py @@ -8,7 +8,8 @@ def _write_executable(path: Path, content: str) -> None: path.write_text(content) - path.chmod(path.stat().st_mode | stat.S_IXUSR) + executable_bits = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + path.chmod(path.stat().st_mode | executable_bits) def test_install_sh_parses_latest_release_tag_on_posix_sed(tmp_path): @@ -50,6 +51,42 @@ def test_install_sh_parses_latest_release_tag_on_posix_sed(tmp_path): -m) printf '%s\n' arm64 ;; *) printf '%s\n' Darwin ;; esac +""", + ) + _write_executable( + fake_bin / "sed", + """#!/usr/bin/env sh +expr="" +while [ "$#" -gt 0 ]; do + case "$1" in + -E) + shift + expr="${1:-}" + ;; + *) + expr="$1" + ;; + esac + shift || break +done + +case "$expr" in + *'[[:space:]]'*) + while IFS= read -r line; do + case "$line" in + *tag_name*) + printf '%s\n' 'v0.1.0' + ;; + *) + printf '%s\n' "$line" + ;; + esac + done + ;; + *) + cat + ;; +esac """, ) _write_executable( @@ -76,7 +113,7 @@ def test_install_sh_parses_latest_release_tag_on_posix_sed(tmp_path): env = { **os.environ, - "PATH": f"{fake_bin}:{os.environ['PATH']}", + "PATH": f"{fake_bin}:{os.environ.get('PATH', '')}", "AGENTRUN_INSTALL": str(install_dir), "AGENTRUN_REPO": "Serverless-Devs/agentrun-cli", }