Files
2026-07-13 13:12:33 +08:00

1237 lines
43 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
import httpx
import pytest
from opensquilla.artifacts import ArtifactStore
from opensquilla.channels.feishu import FeishuChannel, FeishuChannelConfig, _TokenState
from opensquilla.tools.policy import apply_tool_policy_from_config
from opensquilla.tools.registry import (
ToolProfile,
filter_by_profile,
get_default_registry,
profile_allows_tool,
resolve_profile,
)
from opensquilla.tools.types import CallerKind, ToolContext, current_tool_context
def _channel() -> FeishuChannel:
channel = FeishuChannel(
FeishuChannelConfig(app_id="app", app_secret="secret", connection_mode="webhook")
)
channel._token_state = _TokenState(token="tenant-token", expires_at=999999999.0)
return channel
def _channel_tool_context(tmp_path: Path) -> ToolContext:
return ToolContext(
is_owner=False,
caller_kind=CallerKind.CHANNEL,
source_name="feishu-main",
channel_kind="feishu-main",
channel_id="oc_group",
sender_id="ou_sender",
artifact_media_root=str(tmp_path / "media"),
artifact_session_id="session-1",
session_key="agent:main:feishu-main:group:oc_group",
)
def _registered_channel_context(tmp_path: Path) -> ToolContext:
return _channel_tool_context(tmp_path)
@pytest.mark.asyncio
async def test_feishu_chat_reply_uses_native_reply_endpoint() -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
requests: list[httpx.Request] = []
async def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
body = await request.aread()
if request.url.path == "/open-apis/im/v1/messages/om_parent/reply":
payload = json.loads(body)
assert payload["msg_type"] == "text"
assert json.loads(payload["content"]) == {"text": "native reply"}
return httpx.Response(200, json={"code": 0, "data": {"message_id": "om_reply"}})
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
try:
result = json.loads(
await feishu_platform.feishu_chat_reply(
message_id="om_parent",
text="native reply",
channel="feishu-main",
)
)
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result == {
"status": "sent",
"channel": "feishu-main",
"message_id": "om_reply",
"reply_to_message_id": "om_parent",
}
assert [request.url.path for request in requests] == [
"/open-apis/im/v1/messages/om_parent/reply"
]
@pytest.mark.asyncio
async def test_feishu_thread_inbound_reply_uses_native_reply_endpoint() -> None:
from opensquilla.channels.types import OutgoingMessage
requests: list[httpx.Request] = []
async def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
body = await request.aread()
if request.url.path == "/open-apis/im/v1/messages/om_topic/reply":
payload = json.loads(body)
assert payload["msg_type"] == "text"
assert json.loads(payload["content"]) == {"text": "thread reply"}
return httpx.Response(200, json={"code": 0, "data": {"message_id": "om_reply"}})
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
inbound = channel.parse_event(
{
"header": {"event_id": "evt-thread", "event_type": "im.message.receive_v1"},
"event": {
"sender": {"sender_id": {"open_id": "ou_user"}},
"message": {
"message_id": "om_topic",
"chat_id": "oc_topic",
"chat_type": "topic_group",
"thread_id": "omt_topic",
"message_type": "text",
"content": json.dumps({"text": "question"}),
},
},
}
)
reply = channel.build_reply_message("thread reply", inbound)
try:
assert isinstance(reply, OutgoingMessage)
assert reply.metadata["reply_message_id"] == "om_topic"
assert reply.metadata["native_thread_id"] == "omt_topic"
await channel.send(reply)
finally:
await channel.stop()
assert [request.url.path for request in requests] == [
"/open-apis/im/v1/messages/om_topic/reply"
]
@pytest.mark.asyncio
async def test_feishu_chat_send_read_and_edit_call_im_endpoints() -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
seen: list[tuple[str, str]] = []
async def handler(request: httpx.Request) -> httpx.Response:
body = await request.aread()
seen.append((request.method, request.url.path))
if request.method == "POST" and request.url.path == "/open-apis/im/v1/messages":
assert request.url.params["receive_id_type"] == "chat_id"
payload = json.loads(body)
assert payload["receive_id"] == "oc_group"
assert payload["msg_type"] == "text"
assert json.loads(payload["content"]) == {"text": "hello"}
return httpx.Response(200, json={"code": 0, "data": {"message_id": "om_sent"}})
if request.method == "GET" and request.url.path == "/open-apis/im/v1/messages/om_read":
return httpx.Response(
200,
json={"code": 0, "data": {"items": [{"message_id": "om_read"}]}},
)
if request.method == "PUT" and request.url.path == "/open-apis/im/v1/messages/om_edit":
payload = json.loads(body)
assert payload["msg_type"] == "text"
assert json.loads(payload["content"]) == {"text": "updated"}
return httpx.Response(200, json={"code": 0})
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
try:
sent = json.loads(
await feishu_platform.feishu_chat_send(
target="oc_group",
text="hello",
channel="feishu-main",
)
)
read = json.loads(
await feishu_platform.feishu_chat_read(
message_id="om_read",
channel="feishu-main",
)
)
edited = json.loads(
await feishu_platform.feishu_chat_edit(
message_id="om_edit",
text="updated",
channel="feishu-main",
)
)
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
assert sent["message_id"] == "om_sent"
assert read["message"]["items"] == [{"message_id": "om_read"}]
assert edited == {"status": "edited", "channel": "feishu-main", "message_id": "om_edit"}
assert seen == [
("POST", "/open-apis/im/v1/messages"),
("GET", "/open-apis/im/v1/messages/om_read"),
("PUT", "/open-apis/im/v1/messages/om_edit"),
]
@pytest.mark.asyncio
async def test_feishu_doc_wiki_drive_tools_call_platform_endpoints() -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
seen: list[tuple[str, str]] = []
async def handler(request: httpx.Request) -> httpx.Response:
seen.append((request.method, request.url.path))
body = await request.aread()
if request.method == "POST" and request.url.path == "/open-apis/docx/v1/documents":
payload = json.loads(body)
assert payload == {"title": "Launch Notes"}
return httpx.Response(
200,
json={"code": 0, "data": {"document": {"document_id": "doc_token"}}},
)
if request.method == "GET" and request.url.path == (
"/open-apis/docx/v1/documents/doc_token/raw_content"
):
return httpx.Response(
200,
json={"code": 0, "data": {"content": "# Launch Notes"}},
)
if request.method == "POST" and request.url.path == (
"/open-apis/suite/docs-api/search/object"
):
payload = json.loads(body)
assert payload["search_key"] == "Launch"
return httpx.Response(
200,
json={"code": 0, "data": {"files": [{"token": "doc_token"}]}},
)
if request.method == "POST" and request.url.path == "/open-apis/suite/docs-api/meta":
payload = json.loads(body)
assert payload["request_docs"] == [{"docs_token": "doc_token", "docs_type": "docx"}]
return httpx.Response(
200,
json={"code": 0, "data": {"metas": [{"doc_token": "doc_token"}]}},
)
if request.method == "GET" and request.url.path == "/open-apis/wiki/v2/spaces":
return httpx.Response(
200,
json={"code": 0, "data": {"items": [{"space_id": "spc"}]}},
)
if request.method == "GET" and request.url.path == "/open-apis/wiki/v2/spaces/spc/nodes":
assert request.url.params["parent_node_token"] == "parent"
return httpx.Response(
200,
json={"code": 0, "data": {"items": [{"node_token": "node"}]}},
)
if request.method == "GET" and request.url.path == "/open-apis/wiki/v2/spaces/get_node":
assert request.url.params["token"] == "node"
return httpx.Response(
200,
json={"code": 0, "data": {"node": {"node_token": "node"}}},
)
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
try:
created = json.loads(
await feishu_platform.feishu_doc_create(title="Launch Notes", channel="feishu-main")
)
raw = json.loads(
await feishu_platform.feishu_doc_read_raw(
document_id="doc_token",
channel="feishu-main",
)
)
search = json.loads(
await feishu_platform.feishu_drive_search(query="Launch", channel="feishu-main")
)
meta = json.loads(
await feishu_platform.feishu_drive_meta(
doc_token="doc_token",
doc_type="docx",
channel="feishu-main",
)
)
spaces = json.loads(await feishu_platform.feishu_wiki_list_spaces(channel="feishu-main"))
nodes = json.loads(
await feishu_platform.feishu_wiki_list_nodes(
space_id="spc",
parent_node_token="parent",
channel="feishu-main",
)
)
node = json.loads(
await feishu_platform.feishu_wiki_get_node(
token="node",
channel="feishu-main",
)
)
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
assert created["document"]["document_id"] == "doc_token"
assert raw["content"] == "# Launch Notes"
assert search["files"] == [{"token": "doc_token"}]
assert meta["metas"] == [{"doc_token": "doc_token"}]
assert spaces["items"] == [{"space_id": "spc"}]
assert nodes["items"] == [{"node_token": "node"}]
assert node["node"] == {"node_token": "node"}
assert seen == [
("POST", "/open-apis/docx/v1/documents"),
("GET", "/open-apis/docx/v1/documents/doc_token/raw_content"),
("POST", "/open-apis/suite/docs-api/search/object"),
("POST", "/open-apis/suite/docs-api/meta"),
("GET", "/open-apis/wiki/v2/spaces"),
("GET", "/open-apis/wiki/v2/spaces/spc/nodes"),
("GET", "/open-apis/wiki/v2/spaces/get_node"),
]
@pytest.mark.asyncio
async def test_feishu_media_upload_artifact_is_artifact_bound(tmp_path: Path) -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
ctx = _channel_tool_context(tmp_path)
store = ArtifactStore(ctx.artifact_media_root or "")
source = tmp_path / "report.pdf"
source.write_bytes(b"%PDF-1.4\n")
ref = store.publish_file(
source,
session_id=ctx.artifact_session_id or "",
session_key=ctx.session_key or "",
name="report.pdf",
mime="application/pdf",
source="test",
)
requests: list[httpx.Request] = []
async def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
body = await request.aread()
if request.url.path == "/open-apis/im/v1/files":
assert b'file_type"\r\n\r\npdf' in body
assert b"report.pdf" in body
return httpx.Response(200, json={"code": 0, "data": {"file_key": "file-key"}})
if request.url.path == "/open-apis/im/v1/messages":
payload = json.loads(body)
assert payload["receive_id"] == "oc_group"
assert payload["msg_type"] == "file"
assert json.loads(payload["content"]) == {"file_key": "file-key"}
return httpx.Response(200, json={"code": 0, "data": {"message_id": "om_file"}})
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
token = current_tool_context.set(ctx)
try:
result = json.loads(
await feishu_platform.feishu_media_upload_artifact(
artifact_id=ref.id,
)
)
finally:
current_tool_context.reset(token)
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result["status"] == "sent"
assert result["target_id"] == "oc_group"
assert result["provider_file_id"] == "file-key"
assert [request.url.path for request in requests] == [
"/open-apis/im/v1/files",
"/open-apis/im/v1/messages",
]
@pytest.mark.asyncio
async def test_feishu_drive_upload_artifact_is_artifact_bound(tmp_path: Path) -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
ctx = _channel_tool_context(tmp_path)
store = ArtifactStore(ctx.artifact_media_root or "")
source = tmp_path / "report.xlsx"
source.write_bytes(b"xlsx bytes")
ref = store.publish_file(
source,
session_id=ctx.artifact_session_id or "",
session_key=ctx.session_key or "",
name="report.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
source="test",
)
requests: list[httpx.Request] = []
async def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
body = await request.aread()
if request.url.path == "/open-apis/drive/v1/files/upload_all":
assert b'report.xlsx' in body
assert b'parent_type"\r\n\r\nexplorer' in body
assert b'parent_node"\r\n\r\nfld_token' in body
return httpx.Response(
200,
json={"code": 0, "data": {"file_token": "file_token"}},
)
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
token = current_tool_context.set(ctx)
try:
result = json.loads(
await feishu_platform.feishu_drive_upload_artifact(
artifact_id=ref.id,
parent_node="fld_token",
)
)
finally:
current_tool_context.reset(token)
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result["status"] == "uploaded"
assert result["file_token"] == "file_token"
assert [request.url.path for request in requests] == ["/open-apis/drive/v1/files/upload_all"]
@pytest.mark.asyncio
async def test_feishu_drive_upload_missing_scope_returns_diagnostic(tmp_path: Path) -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
ctx = _channel_tool_context(tmp_path)
store = ArtifactStore(ctx.artifact_media_root or "")
source = tmp_path / "report.xlsx"
source.write_bytes(b"xlsx bytes")
ref = store.publish_file(
source,
session_id=ctx.artifact_session_id or "",
session_key=ctx.session_key or "",
name="report.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
source="test",
)
async def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/open-apis/drive/v1/files/upload_all":
return httpx.Response(
400,
json={
"code": 99991672,
"msg": (
"Access denied. One of the following scopes is required: "
"[drive:drive, drive:file, drive:file:upload]."
),
"error": {
"permission_violations": [
{"type": "action_scope_required", "subject": "drive:drive"},
{"type": "action_scope_required", "subject": "drive:file"},
{"type": "action_scope_required", "subject": "drive:file:upload"},
]
},
},
)
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
token = current_tool_context.set(ctx)
try:
result = json.loads(
await feishu_platform.feishu_drive_upload_artifact(
artifact_id=ref.id,
parent_node="fld_token",
)
)
finally:
current_tool_context.reset(token)
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result["status"] == "error"
assert result["error_type"] == "missing_scope"
assert result["diagnostic"]["feature"] == "drive_upload"
assert result["diagnostic"]["required_scopes"] == [
"drive:drive",
"drive:file",
"drive:file:upload",
]
@pytest.mark.asyncio
async def test_feishu_media_upload_rejects_channel_target_override(tmp_path: Path) -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
ctx = _registered_channel_context(tmp_path)
store = ArtifactStore(ctx.artifact_media_root or "")
source = tmp_path / "report.pdf"
source.write_bytes(b"%PDF-1.4\n")
ref = store.publish_file(
source,
session_id=ctx.artifact_session_id or "",
session_key=ctx.session_key or "",
name="report.pdf",
mime="application/pdf",
source="test",
)
feishu_platform.register_feishu_channel("feishu-main", _channel())
token = current_tool_context.set(ctx)
try:
with pytest.raises(Exception, match="target override"):
await feishu_platform.feishu_media_upload_artifact(
artifact_id=ref.id,
target="oc_other",
)
finally:
current_tool_context.reset(token)
feishu_platform.clear_feishu_channels()
@pytest.mark.asyncio
async def test_feishu_media_upload_rejects_channel_session_override(tmp_path: Path) -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
ctx = _registered_channel_context(tmp_path)
store = ArtifactStore(ctx.artifact_media_root or "")
source = tmp_path / "report.pdf"
source.write_bytes(b"%PDF-1.4\n")
ref = store.publish_file(
source,
session_id="other-session",
session_key="agent:main:feishu-main:group:other",
name="report.pdf",
mime="application/pdf",
source="test",
)
feishu_platform.register_feishu_channel("feishu-main", _channel())
token = current_tool_context.set(ctx)
try:
with pytest.raises(Exception, match="session override"):
await feishu_platform.feishu_media_upload_artifact(
artifact_id=ref.id,
session_id="other-session",
)
finally:
current_tool_context.reset(token)
feishu_platform.clear_feishu_channels()
@pytest.mark.asyncio
async def test_feishu_media_upload_rejects_channel_adapter_override(tmp_path: Path) -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
ctx = _registered_channel_context(tmp_path)
store = ArtifactStore(ctx.artifact_media_root or "")
source = tmp_path / "report.pdf"
source.write_bytes(b"%PDF-1.4\n")
ref = store.publish_file(
source,
session_id=ctx.artifact_session_id or "",
session_key=ctx.session_key or "",
name="report.pdf",
mime="application/pdf",
source="test",
)
feishu_platform.register_feishu_channel("feishu-main", _channel())
feishu_platform.register_feishu_channel("feishu-other", _channel())
token = current_tool_context.set(ctx)
try:
with pytest.raises(Exception, match="channel override"):
await feishu_platform.feishu_media_upload_artifact(
artifact_id=ref.id,
channel="feishu-other",
)
finally:
current_tool_context.reset(token)
feishu_platform.clear_feishu_channels()
@pytest.mark.asyncio
async def test_feishu_drive_upload_rejects_channel_session_override(tmp_path: Path) -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
ctx = _registered_channel_context(tmp_path)
store = ArtifactStore(ctx.artifact_media_root or "")
source = tmp_path / "report.xlsx"
source.write_bytes(b"xlsx bytes")
ref = store.publish_file(
source,
session_id="other-session",
session_key="agent:main:feishu-main:group:other",
name="report.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
source="test",
)
feishu_platform.register_feishu_channel("feishu-main", _channel())
token = current_tool_context.set(ctx)
try:
with pytest.raises(Exception, match="session override"):
await feishu_platform.feishu_drive_upload_artifact(
artifact_id=ref.id,
session_id="other-session",
parent_node="fld_token",
)
finally:
current_tool_context.reset(token)
feishu_platform.clear_feishu_channels()
@pytest.mark.asyncio
async def test_feishu_missing_scope_error_returns_stable_diagnostic() -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
async def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/open-apis/im/v1/messages/om_parent/reply":
return httpx.Response(
200,
json={
"code": 99991663,
"msg": "No permissions. Required scopes: im:message:send_as_bot",
},
)
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
try:
result = json.loads(
await feishu_platform.feishu_chat_reply(
message_id="om_parent",
text="reply",
channel="feishu-main",
)
)
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result["status"] == "error"
assert result["error_type"] == "missing_scope"
assert result["diagnostic"]["feature"] == "chat_reply"
assert result["diagnostic"]["required_scopes"] == ["im:message:send_as_bot"]
assert "open.feishu.cn" in result["diagnostic"]["grant_url"]
@pytest.mark.asyncio
async def test_feishu_http_missing_scope_error_returns_stable_diagnostic() -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
async def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/open-apis/wiki/v2/spaces":
return httpx.Response(
400,
json={
"code": 99991672,
"msg": (
"Access denied. One of the following scopes is required: "
"[wiki:wiki:readonly]."
),
"error": {
"permission_violations": [
{
"type": "action_scope_required",
"subject": "wiki:wiki:readonly",
}
]
},
},
)
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
try:
result = json.loads(await feishu_platform.feishu_wiki_list_spaces(channel="feishu-main"))
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result["status"] == "error"
assert result["error_type"] == "missing_scope"
assert result["diagnostic"]["feature"] == "wiki_read"
assert result["diagnostic"]["required_scopes"] == ["wiki:wiki:readonly"]
@pytest.mark.asyncio
async def test_feishu_missing_scope_error_marks_duplicate_notice_with_cooldown(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
calls: list[httpx.Request] = []
async def handler(request: httpx.Request) -> httpx.Response:
calls.append(request)
if request.url.path == "/open-apis/im/v1/messages/om_parent/reply":
return httpx.Response(
200,
json={
"code": 99991663,
"msg": "No permissions. Required scopes: im:message:send_as_bot",
},
)
raise AssertionError(f"unexpected request: {request.method} {request.url}")
ticks = iter([10.0, 11.0, 400.0])
monkeypatch.setattr(feishu_platform, "_monotonic", lambda: next(ticks))
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.clear_feishu_scope_notice_cache()
feishu_platform.register_feishu_channel("feishu-main", channel)
try:
first = json.loads(
await feishu_platform.feishu_chat_reply(
message_id="om_parent",
text="reply",
channel="feishu-main",
)
)
second = json.loads(
await feishu_platform.feishu_chat_reply(
message_id="om_parent",
text="reply",
channel="feishu-main",
)
)
third = json.loads(
await feishu_platform.feishu_chat_reply(
message_id="om_parent",
text="reply",
channel="feishu-main",
)
)
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
feishu_platform.clear_feishu_scope_notice_cache()
assert len(calls) == 3
assert "notice" not in first["diagnostic"]
assert second["diagnostic"]["notice"] == "duplicate_missing_scope"
assert second["diagnostic"]["cooldown_s"] > 0
assert "notice" not in third["diagnostic"]
@pytest.mark.asyncio
async def test_channel_manager_reregisters_feishu_platform_tools_on_restart(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import opensquilla.channels.manager as manager_mod
import opensquilla.tools.builtin.feishu_platform as feishu_platform
channel = _channel()
entry = SimpleNamespace(
enabled=True,
name="feishu-main",
type="feishu",
agent_id="main",
debounce_window_s=0.0,
)
monkeypatch.setattr(manager_mod, "build_managed_channel", lambda _entry: channel)
monkeypatch.setattr(
channel,
"start",
lambda: manager_mod.asyncio.sleep(0),
)
feishu_platform.clear_feishu_channels()
manager = manager_mod.ChannelManager.from_config(
[entry],
turn_runner=None,
session_manager=None,
)
try:
await manager.restart_channel("feishu-main")
result = json.loads(await feishu_platform.feishu_scopes_status(channel="feishu-main"))
finally:
await manager.stop_channel("feishu-main")
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result["status"] == "ok"
@pytest.mark.asyncio
async def test_channel_manager_stop_all_unregisters_channels_without_tasks(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import opensquilla.channels.manager as manager_mod
import opensquilla.tools.builtin.feishu_platform as feishu_platform
channel = _channel()
entry = SimpleNamespace(
enabled=True,
name="feishu-main",
type="feishu",
agent_id="main",
debounce_window_s=0.0,
)
monkeypatch.setattr(manager_mod, "build_managed_channel", lambda _entry: channel)
feishu_platform.clear_feishu_channels()
manager = manager_mod.ChannelManager.from_config(
[entry],
turn_runner=None,
session_manager=None,
)
try:
await manager.stop_all()
with pytest.raises(Exception, match="unknown Feishu channel"):
await feishu_platform.feishu_scopes_status(channel="feishu-main")
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
def test_feishu_scopes_status_is_channel_default_visible() -> None:
import opensquilla.tools.builtin # noqa: F401
registry = get_default_registry()
channel_ctx = ToolContext(is_owner=False, caller_kind=CallerKind.CHANNEL)
names = {
tool.name
for tool in filter_by_profile(
registry.to_tool_definitions(channel_ctx),
resolve_profile(channel_ctx),
channel_ctx,
)
}
assert "feishu_scopes_status" in names
assert "feishu_doc_create" in names
assert "feishu_doc_read_raw" in names
assert "feishu_doc_list_blocks" in names
assert "feishu_drive_meta" in names
assert "feishu_drive_search" in names
assert "feishu_drive_upload_artifact" in names
assert "feishu_wiki_get_node" in names
assert "feishu_wiki_list_nodes" in names
assert "feishu_wiki_list_spaces" in names
assert "feishu_perm_grant" not in names
assert "feishu_perm_grant_member" not in names
assert "write_file" not in names
assert "execute_code" not in names
assert "apply_patch" not in names
def test_feishu_capability_manifest_matches_channel_default_visibility() -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
for feature, capability in feishu_platform._FEATURE_CAPABILITIES.items():
for tool_name in capability.tools:
assert (
profile_allows_tool(tool_name, ToolProfile.CHANNEL_DEFAULT)
is capability.default_channel_visible
), f"{feature}:{tool_name}"
def test_channel_scopes_policy_expands_feishu_diagnostics() -> None:
import opensquilla.tools.builtin # noqa: F401
registry = get_default_registry()
config = {
"channels": {
"feishu": {
"groups": {
"*": {
"tools": {
"profile": "minimal",
"also_allow": ["channel:scopes"],
}
}
}
}
}
}
ctx = apply_tool_policy_from_config(
ToolContext(is_owner=False, caller_kind=CallerKind.CHANNEL, channel_kind="feishu"),
available_tools=registry.list_names(),
config=config,
)
assert "feishu_scopes_status" in (ctx.allowed_tools or set())
def test_channel_chat_policy_does_not_grant_privileged_feishu_platform_tools() -> None:
import opensquilla.tools.builtin # noqa: F401
registry = get_default_registry()
config = {
"channels": {
"feishu": {
"groups": {
"*": {
"tools": {
"profile": "minimal",
"also_allow": ["channel:chat"],
}
}
}
}
}
}
ctx = apply_tool_policy_from_config(
ToolContext(is_owner=False, caller_kind=CallerKind.CHANNEL, channel_kind="feishu"),
available_tools=registry.list_names(),
config=config,
)
assert "feishu_chat_reply" not in (ctx.allowed_tools or set())
assert "feishu_chat_send" not in (ctx.allowed_tools or set())
assert "feishu_chat_read" not in (ctx.allowed_tools or set())
assert "feishu_chat_edit" not in (ctx.allowed_tools or set())
def test_channel_doc_wiki_drive_policy_grants_platform_safe_tools_only() -> None:
import opensquilla.tools.builtin # noqa: F401
registry = get_default_registry()
config = {
"channels": {
"feishu": {
"groups": {
"*": {
"tools": {
"profile": "minimal",
"also_allow": ["channel:doc", "channel:wiki", "channel:drive"],
}
}
}
}
}
}
ctx = apply_tool_policy_from_config(
ToolContext(is_owner=False, caller_kind=CallerKind.CHANNEL, channel_kind="feishu"),
available_tools=registry.list_names(),
config=config,
)
allowed = ctx.allowed_tools or set()
assert "feishu_doc_create" in allowed
assert "feishu_doc_read_raw" in allowed
assert "feishu_drive_upload_artifact" in allowed
assert "feishu_drive_search" in allowed
assert "feishu_wiki_list_spaces" in allowed
assert "read_file" not in allowed
assert "write_file" not in allowed
assert "execute_code" not in allowed
assert "publish_artifact" not in allowed
def test_channel_perm_policy_is_sender_scoped() -> None:
import opensquilla.tools.builtin # noqa: F401
registry = get_default_registry()
config = {
"channels": {
"feishu": {
"groups": {
"*": {
"tools": {
"profile": "minimal",
"allow": ["feishu_perm_grant_member"],
"also_allow": ["channel:perm"],
}
},
"oc_group": {
"tools": {
"toolsBySender": {
"id:ou_allowed": {
"also_allow": ["channel:perm"],
}
}
}
},
}
}
}
}
default_ctx = apply_tool_policy_from_config(
ToolContext(
is_owner=False,
caller_kind=CallerKind.CHANNEL,
channel_kind="feishu",
channel_id="oc_group",
sender_id="ou_other",
),
available_tools=registry.list_names(),
config=config,
)
sender_ctx = apply_tool_policy_from_config(
ToolContext(
is_owner=False,
caller_kind=CallerKind.CHANNEL,
channel_kind="feishu",
channel_id="oc_group",
sender_id="ou_allowed",
),
available_tools=registry.list_names(),
config=config,
)
assert "feishu_perm_grant_member" not in (default_ctx.allowed_tools or set())
assert "feishu_perm_grant_member" in (sender_ctx.allowed_tools or set())
@pytest.mark.asyncio
async def test_feishu_perm_grant_member_dry_run_does_not_call_api() -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
async def handler(request: httpx.Request) -> httpx.Response:
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
try:
result = json.loads(
await feishu_platform.feishu_perm_grant_member(
token="doc_token",
doc_type="docx",
member_type="openid",
member_id="ou_user",
perm="view",
channel="feishu-main",
)
)
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result["status"] == "dry_run"
assert result["operation"] == "grant_member"
assert result["plan"]["path"] == "/drive/v1/permissions/doc_token/members"
assert result["plan"]["body"] == {
"member_type": "openid",
"member_id": "ou_user",
"perm": "view",
}
@pytest.mark.asyncio
async def test_feishu_perm_grant_member_mutates_after_dry_run_opt_in() -> None:
import opensquilla.tools.builtin.feishu_platform as feishu_platform
seen: list[httpx.Request] = []
async def handler(request: httpx.Request) -> httpx.Response:
seen.append(request)
body = await request.aread()
if request.url.path == "/open-apis/drive/v1/permissions/doc_token/members":
assert request.url.params["type"] == "docx"
assert request.url.params["need_notification"] == "false"
assert json.loads(body) == {
"member_type": "openid",
"member_id": "ou_user",
"perm": "edit",
}
return httpx.Response(
200,
json={
"code": 0,
"data": {
"member": {
"member_type": "openid",
"member_id": "ou_user",
"perm": "edit",
}
},
},
)
raise AssertionError(f"unexpected request: {request.method} {request.url}")
channel = _channel()
channel._client = httpx.AsyncClient(
base_url="https://open.feishu.cn/open-apis",
transport=httpx.MockTransport(handler),
)
feishu_platform.register_feishu_channel("feishu-main", channel)
try:
result = json.loads(
await feishu_platform.feishu_perm_grant_member(
token="doc_token",
doc_type="docx",
member_type="openid",
member_id="ou_user",
perm="edit",
dry_run=False,
channel="feishu-main",
)
)
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
assert [request.url.path for request in seen] == [
"/open-apis/drive/v1/permissions/doc_token/members"
]
assert result["status"] == "granted"
assert result["member"]["member_id"] == "ou_user"
@pytest.mark.asyncio
async def test_channel_manager_registers_feishu_platform_tools(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import opensquilla.channels.manager as manager_mod
import opensquilla.tools.builtin.feishu_platform as feishu_platform
channel = _channel()
entry = SimpleNamespace(
enabled=True,
name="feishu-main",
type="feishu",
agent_id="main",
debounce_window_s=0.0,
)
monkeypatch.setattr(manager_mod, "build_managed_channel", lambda _entry: channel)
feishu_platform.clear_feishu_channels()
try:
manager_mod.ChannelManager.from_config(
[entry],
turn_runner=None,
session_manager=None,
)
result = json.loads(await feishu_platform.feishu_scopes_status(channel="feishu-main"))
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()
assert result["status"] == "ok"
assert result["channel"] == "feishu-main"
assert "wiki:space:retrieve" in result["features"]["wiki_read"]["required_scopes"]
assert "drive:drive" in result["features"]["drive_upload"]["required_scopes"]
assert result["features"]["doc_create"]["tools"] == ["feishu_doc_create"]
assert result["features"]["doc_create"]["category"] == "doc"
assert result["features"]["doc_create"]["mutates"] is True
assert result["features"]["wiki_read"]["category"] == "wiki"
assert result["features"]["wiki_read"]["mutates"] is False
assert result["features"]["perm_member"]["category"] == "perm"
assert result["features"]["perm_member"]["mutates"] is True
assert result["features"]["perm_member"]["dry_run_supported"] is True
assert result["features"]["perm_member"]["default_channel_visible"] is False
@pytest.mark.asyncio
async def test_channel_manager_unregisters_feishu_platform_tools_on_stop(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import opensquilla.channels.manager as manager_mod
import opensquilla.tools.builtin.feishu_platform as feishu_platform
channel = _channel()
entry = SimpleNamespace(
enabled=True,
name="feishu-main",
type="feishu",
agent_id="main",
debounce_window_s=0.0,
)
monkeypatch.setattr(manager_mod, "build_managed_channel", lambda _entry: channel)
feishu_platform.clear_feishu_channels()
manager = manager_mod.ChannelManager.from_config(
[entry],
turn_runner=None,
session_manager=None,
)
try:
await manager.stop_channel("feishu-main")
with pytest.raises(Exception, match="unknown Feishu channel"):
await feishu_platform.feishu_scopes_status(channel="feishu-main")
finally:
await channel.stop()
feishu_platform.clear_feishu_channels()