"""CLI tests for `opensquilla channels`.""" from __future__ import annotations from pathlib import Path from typer.testing import CliRunner from opensquilla.cli.main import app runner = CliRunner() def _setenv(monkeypatch, tmp_path: Path) -> Path: target = tmp_path / "c.toml" monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target)) return target def test_channels_list_empty(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke(app, ["channels", "list"]) assert result.exit_code == 0 assert "0 channels" in result.stdout.lower() def test_channels_add_telegram_polling_minimal(tmp_path, monkeypatch): target = _setenv(monkeypatch, tmp_path) result = runner.invoke( app, ["channels", "add", "telegram", "--name", "tg", "--token", "abc"], ) assert result.exit_code == 0, result.stdout text = target.read_text() assert "tg" in text assert "telegram" in text assert "abc" not in result.stdout assert "restart" in result.stdout.lower() def test_channels_add_slack_missing_token_fails(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke(app, ["channels", "add", "slack", "--name", "w"]) assert result.exit_code != 0 combined = (result.stdout + (result.stderr or "")).lower() assert "token" in combined def test_channels_add_slack_succeeds_with_token(tmp_path, monkeypatch): target = _setenv(monkeypatch, tmp_path) result = runner.invoke( app, [ "channels", "add", "slack", "--name", "w", "--token", "xoxb-x", "--field", "signing_secret=ss", "--field", "slack_channel_id=C123", ], ) assert result.exit_code == 0, result.stdout text = target.read_text() assert "C123" in text assert "xoxb-x" not in result.stdout def test_channels_remove(tmp_path, monkeypatch): target = _setenv(monkeypatch, tmp_path) runner.invoke(app, ["channels", "add", "slack", "--name", "w", "--token", "x", "--field", "signing_secret=ss"]) result = runner.invoke(app, ["channels", "remove", "w"]) assert result.exit_code == 0 # Either the channel is gone, or the [[channels.channels]] table is empty. text = target.read_text() assert 'name = "w"' not in text def test_channels_disable_then_enable(tmp_path, monkeypatch): target = _setenv(monkeypatch, tmp_path) runner.invoke(app, ["channels", "add", "slack", "--name", "w", "--token", "x", "--field", "signing_secret=ss"]) r1 = runner.invoke(app, ["channels", "disable", "w"]) assert r1.exit_code == 0 assert "enabled = false" in target.read_text() r2 = runner.invoke(app, ["channels", "enable", "w"]) assert r2.exit_code == 0 assert "enabled = true" in target.read_text() def test_channels_list_redacts_secrets(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) runner.invoke(app, ["channels", "add", "slack", "--name", "w", "--token", "supersecret", "--field", "signing_secret=ss"]) result = runner.invoke(app, ["channels", "list"]) assert "supersecret" not in result.stdout assert "***" in result.stdout def test_channels_edit_updates_only_provided_fields(tmp_path, monkeypatch): target = _setenv(monkeypatch, tmp_path) runner.invoke( app, ["channels", "add", "slack", "--name", "w", "--token", "xoxb-original", "--field", "signing_secret=ss", "--field", "slack_channel_id=C111"], ) result = runner.invoke( app, ["channels", "edit", "w", "--field", "slack_channel_id=C222"], ) assert result.exit_code == 0, result.stdout text = target.read_text() assert "C222" in text assert "C111" not in text assert "xoxb-original" in text def test_channels_edit_unknown_name_fails(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke(app, ["channels", "edit", "missing"]) assert result.exit_code != 0 def test_channels_edit_preserves_enabled_when_unchanged(tmp_path, monkeypatch): target = _setenv(monkeypatch, tmp_path) runner.invoke( app, ["channels", "add", "slack", "--name", "w", "--token", "xoxb-x", "--field", "signing_secret=ss", "--disabled"], ) assert "enabled = false" in target.read_text() r = runner.invoke( app, ["channels", "edit", "w", "--field", "slack_channel_id=C9"] ) assert r.exit_code == 0, r.stdout text = target.read_text() assert "enabled = false" in text assert "C9" in text def test_channels_edit_preserves_agent_id_when_unchanged(tmp_path, monkeypatch): target = _setenv(monkeypatch, tmp_path) runner.invoke( app, ["channels", "add", "slack", "--name", "w", "--token", "xoxb-x", "--field", "signing_secret=ss", "--agent-id", "ops"], ) assert 'agent_id = "ops"' in target.read_text() r = runner.invoke( app, ["channels", "edit", "w", "--field", "slack_channel_id=C9"] ) assert r.exit_code == 0, r.stdout assert 'agent_id = "ops"' in target.read_text() def test_channels_edit_preserves_non_secret_bool_when_unchanged( tmp_path, monkeypatch ): target = _setenv(monkeypatch, tmp_path) runner.invoke( app, ["channels", "add", "slack", "--name", "w", "--token", "xoxb-x", "--field", "signing_secret=ss", "--field", "reply_in_thread=true"], ) assert "reply_in_thread = true" in target.read_text() r = runner.invoke( app, ["channels", "edit", "w", "--field", "slack_channel_id=C9"] ) assert r.exit_code == 0, r.stdout assert "reply_in_thread = true" in target.read_text() def test_channels_add_token_resolves_to_alias_order_not_field_order( tmp_path, monkeypatch ): """For wecom, --token should resolve to the literal 'token' field (alias index 0), not 'corp_secret' (alias index 5) which would happen under naive spec-field-order resolution. """ _setenv(monkeypatch, tmp_path) result = runner.invoke( app, ["channels", "add", "wecom", "--name", "wc", "--token", "wecom-token", "--field", "corp_id=cid", "--field", "corp_secret=cs", "--field", "agent_id_int=1000", "--field", "encoding_aes_key=" + "a" * 43], ) assert result.exit_code == 0, result.stdout assert "wecom.token" in result.stdout assert "wecom.corp_secret" not in result.stdout def test_channels_add_token_flag_echoes_resolved_field(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke( app, ["channels", "add", "matrix", "--name", "m", "--token", "syt-abc", "--field", "homeserver_url=https://matrix.org", "--field", "user_id=@me:matrix.org"] ) assert result.exit_code == 0, result.stdout assert "--token" in result.stdout assert "matrix.access_token" in result.stdout def test_channels_add_token_flag_for_slack_resolves_to_token(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke( app, [ "channels", "add", "slack", "--name", "w", "--token", "xoxb-x", "--field", "signing_secret=ss", ], ) assert result.exit_code == 0, result.stdout assert "slack.token" in result.stdout def test_channels_describe_slack(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke(app, ["channels", "describe", "slack"]) assert result.exit_code == 0 out = result.stdout assert "token" in out assert "signing_secret" in out assert "webhook" in out.lower() assert "api.slack.com" in out def test_channels_describe_unknown_type_fails(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke(app, ["channels", "describe", "no-such-type"]) assert result.exit_code != 0 def test_channels_types_lists_all_supported(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke(app, ["channels", "types"]) assert result.exit_code == 0 out = result.stdout for t in ("slack", "telegram", "discord", "feishu", "dingtalk", "wecom", "qq", "matrix"): assert t in out # msteams is intentionally hidden from the channel catalog CLI surface # until first-class support lands. assert "msteams" not in out assert "transport" in out.lower() def test_channels_add_restart_notice_disambiguates(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke( app, [ "channels", "add", "slack", "--name", "w", "--token", "x", "--field", "signing_secret=ss", ], ) assert result.exit_code == 0 out = result.stdout.lower() assert "gateway process" in out assert "not the same as" in out assert "channels restart" in out def test_channels_add_prints_status_verification_next_step(tmp_path, monkeypatch): _setenv(monkeypatch, tmp_path) result = runner.invoke( app, [ "channels", "add", "slack", "--name", "w", "--token", "x", "--field", "signing_secret=ss", ], ) assert result.exit_code == 0 out = result.stdout.lower() assert "opensquilla gateway restart" in out assert "opensquilla channels status w --json" in out def test_channels_add_echoes_resolved_path_and_source(tmp_path, monkeypatch): monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False) monkeypatch.setenv("HOME", str(tmp_path / "home")) project = tmp_path / "project" project.mkdir() (project / "opensquilla.toml").write_text("") monkeypatch.chdir(project) result = runner.invoke( app, [ "channels", "add", "slack", "--name", "w", "--token", "x", "--field", "signing_secret=ss", ], ) assert result.exit_code == 0, result.stdout assert str(project / "opensquilla.toml") in result.stdout assert "cwd" in result.stdout.lower()