Files
Octop/tests/unit/bridge/test_bridge_proxy_local.py
0394e28fdc fix(bridge): keep team member streams alive through refresh (#2159)
* fix(bridge): keep team member streams alive through refresh

Cloud rooms dropped member tokens after the host turn ended, and a refresh mid-answer had nothing in history to restore. Hold the room subscriber and replay the in-progress member text when a client subscribes again.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: note cloud team stream refresh in the changelog

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(bridge): keep local login when peer credentials are rejected

Peer auth failure used HTTP 401, so probing cloud collab cleared the local session and showed Unauthorized.

Fixes #2166

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(dashboard): split chat nav into experts, teams, and cloud

Empty team and cloud lists can jump to create the first one, the host wrap-up lines up with the host avatar, and the cloud collab page explains how to connect.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(dashboard): add knowledge-base folder tree and unify list-panel rail toggles

- Knowledge bases: add a resizable folder navigation tree beside the document
  list; move the breadcrumb into the docs toolbar and surface base id plus
  creator on list rows.
- Knowledge bases / skill packages: move the collapse control onto the split
  divider as a hover pill and drop the separate detail header.
- RailEdgeControl: pill button with center/before/after placement so two rails
  can share one divider.

---------

Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 20:05:30 +08:00

120 lines
4.1 KiB
Python

"""Unit tests for hub-local Bridge shadow history-migration responses."""
from __future__ import annotations
import json
import pytest
from octop.api.middleware.bridge_proxy import (
_local_bridge_shadow_response,
resolve_tunnel_target,
)
from octop.infra.errors import ErrorCode, OctopError
def test_history_migration_status_is_idle() -> None:
resp = _local_bridge_shadow_response(
method="GET",
rest="/history-migration/status",
)
assert resp is not None
body = json.loads(resp.body)
assert body["remaining"] == 0
assert body["can_start"] is False
def test_history_migration_start_rejected() -> None:
with pytest.raises(OctopError) as ei:
_local_bridge_shadow_response(
method="POST",
rest="/history-migration/start",
)
assert ei.value.code == ErrorCode.BRIDGE_REMOTE_UNSUPPORTED
def test_status_is_not_short_circuited() -> None:
"""Runtime status must tunnel to the peer (allowlisted), not be faked here."""
assert (
_local_bridge_shadow_response(
method="GET",
rest="/status",
)
is None
)
def test_other_paths_pass_through() -> None:
assert (
_local_bridge_shadow_response(
method="GET",
rest="/threads",
)
is None
)
def test_resolve_upload_path() -> None:
target = resolve_tunnel_target("/api/agents/bridge:cid1:aid1/upload")
assert target is not None
assert target.remote_path == "/api/agents/aid1/upload"
assert target.agent_rest == "/upload"
def test_resolve_agents_and_plugins_path() -> None:
agents = resolve_tunnel_target("/api/agents/bridge:cid1:aid1/tool-settings")
assert agents is not None
assert agents.remote_path == "/api/agents/aid1/tool-settings"
assert agents.ref.connection_id == "cid1"
plugins = resolve_tunnel_target("/api/plugins/agents/bridge:cid1:aid1/tools")
assert plugins is not None
assert plugins.remote_path == "/api/plugins/agents/aid1/tools"
def test_resolve_unwraps_stale_shadow_thread_id() -> None:
target = resolve_tunnel_target(
"/api/agents/bridge:cid1:doctor/threads/01ROOM~bridge:cid1:doctor/history"
)
assert target is not None
assert target.remote_path == "/api/agents/doctor/threads/01ROOM~doctor/history"
encoded = resolve_tunnel_target(
"/api/agents/bridge%3Acid1%3Adoctor/threads/01ROOM~bridge%3Acid1%3Adoctor/history"
)
assert encoded is not None
assert encoded.remote_path == "/api/agents/doctor/threads/01ROOM~doctor/history"
def test_resolve_header_tunneled_paths() -> None:
mbti = resolve_tunnel_target("/api/mbti/current", "bridge:cid1:aid1")
assert mbti is not None
assert mbti.remote_path == "/api/mbti/current"
assert mbti.ref.remote_agent_id == "aid1"
catalog = resolve_tunnel_target("/api/subagent-catalog/divisions", "bridge:cid1:aid1")
assert catalog is not None
acp = resolve_tunnel_target("/api/acp", "bridge:cid1:aid1")
assert acp is not None
cron = resolve_tunnel_target("/api/cron/settings", "bridge:cid1:aid1")
assert cron is not None
assert cron.remote_path == "/api/cron/settings"
connectors = resolve_tunnel_target("/api/connector-instances", "bridge:cid1:aid1")
assert connectors is not None
models = resolve_tunnel_target("/api/providers/resolved", "bridge:cid1:aid1")
assert models is not None
assert models.remote_path == "/api/providers/resolved"
kb = resolve_tunnel_target("/api/knowledge-bases", "bridge:cid1:aid1")
assert kb is not None
env = resolve_tunnel_target("/api/browser/env-status", "bridge:cid1:aid1")
assert env is not None
hitl = resolve_tunnel_target("/api/settings/hitl", "bridge:cid1:aid1")
assert hitl is not None
assert hitl.remote_path == "/api/settings/hitl"
def test_resolve_ignores_local_header_and_unscoped_paths() -> None:
assert resolve_tunnel_target("/api/mbti/current", "01LOCAL") is None
assert resolve_tunnel_target("/api/users", "bridge:cid1:aid1") is None
assert resolve_tunnel_target("/api/plugins/install", "bridge:cid1:aid1") is None