from __future__ import annotations import json import pytest from yuxi.channels.meta import ChannelMeta from yuxi.channels.models import ChannelAccountSnapshot, TokenStatus class TestChannelMetaNewFields: def test_all_fields_have_defaults(self): meta = ChannelMeta(id="test", label="Test") assert meta.selection_docs_prefix == "" assert meta.selection_docs_omit_label is False assert meta.detail_label == "" assert meta.prefer_session_lookup_for_announce_target is False assert meta.prefer_over is False def test_full_meta_with_new_fields(self): meta = ChannelMeta( id="discord", label="Discord", selection_label="Discord Bot", blurb="Discord 渠道适配器", order=3, docs_path="docs/channels/discord", docs_label="Discord 文档", selection_docs_prefix="/channels", selection_docs_omit_label=False, system_image="discord", markdown_capable=True, detail_label="Discord 详情", aliases=["dc"], exposure="public", show_configured=True, show_in_setup=True, quickstart_allow_from=["web", "mobile"], force_account_binding=False, prefer_session_lookup_for_announce_target=True, prefer_over=True, ) assert meta.id == "discord" assert meta.selection_docs_prefix == "/channels" assert meta.selection_docs_omit_label is False assert meta.detail_label == "Discord 详情" assert meta.prefer_session_lookup_for_announce_target is True assert meta.prefer_over is True def test_model_dump_includes_new_fields(self): meta = ChannelMeta( id="telegram", label="Telegram", detail_label="TG", prefer_over=True, ) dumped = meta.model_dump() assert "detail_label" in dumped assert "prefer_over" in dumped assert "selection_docs_prefix" in dumped assert "selection_docs_omit_label" in dumped assert "prefer_session_lookup_for_announce_target" in dumped def test_model_dump_serializable(self): meta = ChannelMeta( id="test", label="Test", prefer_over=True, detail_label="Detail", ) json.dumps(meta.model_dump()) def test_wechat_meta_backward_compat(self): from yuxi.channels.adapters.wechat.capabilities import WECHAT_META, get_meta meta = get_meta() assert isinstance(meta, ChannelMeta) assert meta.id == "wechat" assert meta.label == "微信" assert meta.order == 8 assert meta.aliases == ["weixin", "WeChat"] assert meta.markdown_capable is False assert meta.exposure == "configured" dumped = WECHAT_META.model_dump() assert dumped["id"] == "wechat" assert dumped["label"] == "微信" def test_roundtrip_serialization(self): original = ChannelMeta( id="line", label="LINE", selection_label="LINE Bot", docs_path="docs/channels/line", blurb="LINE 渠道适配器", order=10, aliases=["line_me"], system_image="line", markdown_capable=False, exposure="public", detail_label="LINE", prefer_over=False, prefer_session_lookup_for_announce_target=False, ) data = original.model_dump() restored = ChannelMeta(**data) assert restored == original class TestTokenStatus: def test_default_values(self): ts = TokenStatus() assert ts.source == "" assert ts.status == "unknown" assert ts.last_verified_at == 0.0 def test_custom_values(self): ts = TokenStatus(source="env", status="valid", last_verified_at=1715000000.0) assert ts.source == "env" assert ts.status == "valid" assert ts.last_verified_at == 1715000000.0 def test_model_dump_serializable(self): ts = TokenStatus(source="vault", status="expired", last_verified_at=1715000000.0) json.dumps(ts.model_dump()) class TestChannelAccountSnapshotNewFields: def test_default_token_fields(self): snap = ChannelAccountSnapshot() assert snap.token_source == "" assert snap.token_status is None assert snap.bot_token_source == "" assert snap.bot_token_status is None assert snap.app_token_source == "" assert snap.app_token_status is None assert snap.signing_secret_source == "" assert snap.signing_secret_status is None assert snap.user_token_status is None assert snap.credential_source == "" assert snap.secret_source == "" def test_default_network_fields(self): snap = ChannelAccountSnapshot() assert snap.webhook_path == "" assert snap.webhook_url == "" assert snap.base_url == "" assert snap.cli_path == "" assert snap.db_path == "" assert snap.port == 0 def test_default_probe_fields(self): snap = ChannelAccountSnapshot() assert snap.probe is None assert snap.last_probe_at == 0.0 assert snap.audit is None def test_default_bot_fields(self): snap = ChannelAccountSnapshot() assert snap.application is None assert snap.bot is None assert snap.public_key == "" assert snap.profile is None def test_token_status_with_data(self): snap = ChannelAccountSnapshot( account_id="acc_1", token_source="env", token_status=TokenStatus(source="env", status="valid"), bot_token_source="config", bot_token_status=TokenStatus(source="config", status="expired"), ) assert snap.token_source == "env" assert snap.token_status.source == "env" assert snap.token_status.status == "valid" assert snap.bot_token_status.status == "expired" def test_network_fields_with_data(self): snap = ChannelAccountSnapshot( account_id="acc_1", webhook_path="/webhook/whatsapp", webhook_url="https://example.com/webhook", base_url="https://api.example.com", port=8443, ) assert snap.webhook_path == "/webhook/whatsapp" assert snap.webhook_url == "https://example.com/webhook" assert snap.base_url == "https://api.example.com" assert snap.port == 8443 def test_probe_fields_with_data(self): snap = ChannelAccountSnapshot( account_id="acc_1", probe={"endpoint": "/health", "timeout": 5}, last_probe_at=1715000000.0, audit={"last_audit": "2024-01-01"}, ) assert snap.probe == {"endpoint": "/health", "timeout": 5} assert snap.last_probe_at == 1715000000.0 assert snap.audit == {"last_audit": "2024-01-01"} def test_bot_fields_with_data(self): snap = ChannelAccountSnapshot( account_id="acc_1", application={"name": "MyBot", "version": "1.0"}, bot={"username": "my_bot", "id": "123"}, public_key="ssh-rsa AAA...", profile={"display_name": "My Bot"}, ) assert snap.application == {"name": "MyBot", "version": "1.0"} assert snap.bot == {"username": "my_bot", "id": "123"} assert snap.public_key == "ssh-rsa AAA..." assert snap.profile == {"display_name": "My Bot"} def test_snapshot_dump_includes_new_fields(self): snap = ChannelAccountSnapshot( account_id="acc_1", token_source="env", webhook_path="/wh", probe={"key": "val"}, public_key="abc", ) dumped = snap.model_dump() assert "token_source" in dumped assert "webhook_path" in dumped assert "probe" in dumped assert "public_key" in dumped def test_snapshot_backward_compat_core_fields(self): snap = ChannelAccountSnapshot(account_id="acc_1", name="test", configured=True) assert snap.account_id == "acc_1" assert snap.name == "test" assert snap.configured is True assert snap.enabled is True assert snap.linked is False def test_snapshot_extra_fields_still_allowed(self): snap = ChannelAccountSnapshot(account_id="acc_1", custom_field="custom_value") assert snap.custom_field == "custom_value" def test_snapshot_json_serializable(self): snap = ChannelAccountSnapshot( account_id="acc_1", token_status=TokenStatus(source="env", status="valid"), probe={"key": "val"}, bot={"id": "123"}, ) json.dumps(snap.model_dump()) class TestWhatsAppChannelMetaMigration: def test_whatsapp_channel_meta_import_works(self): from yuxi.channels.adapters.whatsapp.channel_meta import ( ChannelCapability, ChannelMeta, ChannelMode, WhatsAppChannelMeta, ) assert ChannelMeta is WhatsAppChannelMeta def test_whatsapp_channel_meta_has_capability(self): from yuxi.channels.adapters.whatsapp.channel_meta import ( ChannelCapability, WhatsAppChannelMeta, ) meta = WhatsAppChannelMeta() assert meta.has_capability(ChannelCapability.TEXT) is True assert meta.has_capability(ChannelCapability.REACTION) is False def test_whatsapp_adapter_has_display_meta(self): from yuxi.channels.adapters.whatsapp.adapter import WhatsAppAdapter from yuxi.channels.meta import ChannelMeta as DisplayMeta assert isinstance(WhatsAppAdapter.meta, DisplayMeta) assert WhatsAppAdapter.meta.id == "whatsapp" assert WhatsAppAdapter.meta.label == "WhatsApp"